<?php
/*  ================================================================== *\
    (C) Copyright 2010 by Kenneth Downs

    This file is part of Triangulum
\*  ================================================================== */
        
/**
 * Run-Time tests Manager
 * 
 * It is assumed this program is running under Windows.  No attempt
 * is made to make the code compatible with Linux.
 * 
 * This program expects certain DSNs to exist.  It uses PDO
 * and ODBC to 
 * connect to all databases.  Each DSN is named "tri-" plus a 
 * platform name plus "master" or "tritests".  The "master" DSN
 * is for connecting to the server's master database and dropping
 * and recreating the "tritest" database.  The "tritest" database
 * is for actual runtime tests.  The complete list of expected
 * DSNs is:
 * - tri-db2-master: DB2, Connect to "postgres" database
 * - tri-db2-tritests: DB2, Connect to "tritests" database  
 * - tri-mssql-master: SQL Server, Connect to "postgres" database
 * - tri-mssql-tests: SQL Server,Connect to "tritests" database  
 * - tri-my-master: MySQL, Connect to "postgres" database
 * - tri-my-tritests: MySQL, Connect to "tritests" database  
 * - tri-oci-master: Oracle, Connect to "postgres" database
 * - tri-oci-tritests: Oracle, Connect to "tritests" database  
 * - tri-pg-master: Oracle, Connect to "postgres" database
 * - tri-pg-tritests: Oracle, Connect to "tritests" database  
 *  
 * Usernames on all servers are expected to be "triangulum", with
 * a password of "triangulum" or "triangulum99".
 *
 * @package Triangulum
 * @author Ken Downs <ken@secdat.com>
 * 
 */

ini_set('display_errors',E_ALL);
$tr = new trTestsRuntime;
$tr->main();

class trTestsRuntime {
    /**
     * @var string Global PDO connector for current platform
     */
    var $dbh = false;
    
    /**
     * @var $Array hardcoded list of platforms to support
     */
    # Need major re-thinking of create database/user/schema
    #var $platforms = array('mssql', 'mysql', 'postgres');
    var $platforms = array('postgres');
    
    
    var $results = array();
    
    var $out = '';
    
    /**
     *  Contains original errors by platform before they are
     *  mapped to universal numbers.  This lets us look at them
     *  and compare details that we have to remove for the
     *  comparison tests.
     *
     */
    var $err = '';
    
    private $errmaps = array(
        'mysql'=>array(
            '23000'=>'23000'
           ,'42000'=>'CUSTOM'
        ),
        'mssql'=>array(
            '23000'=>'23000'
           ,'42000'=>'CUSTOM'
        ),
        'postgres'=>array(
            '23505'=>'23000'
           ,'P0001'=>'CUSTOM'
        )
    );
    
    /**
     * Main processing routine for runtime tests
     * 
     * This is the comprehensive testing program for runtime tests.
     * Runtime tests issue commands to a test database, and if the
     * output matches what is expected, the test passes.
     * 
     * Debugging tests are managed by {@link tests-debug.php}.
     *
     * The complete testing program follows these steps:
     * - Test master DSN connections and drop/recreate "tritests"
     *   database on each platform.  Handled by {@link mainTestsConnect}.
     *   
     *   
     * - Assembles a spec file out of all tests in the "tests-runtime"
     *   subdirectory.  Handled by {@link mainAssemble}.
     * - Generates a build script.  Handled in {@link main}
     * - For all platforms, drops and recreates the "androv2" database
     *   and then runs the build script.  Handled in {@link main}.
     * - recurses through tests for all platforms.  Handled in
     *   {@link mainTests}.
     * - report results.  Handled in {@link main}.
     *
     * Each subdirectory of "tests-runtime" contains one test.  It must
     * contain a dd.yaml file with the same name as the subdirectory.
     * Each test contains a text file, "desc.txt", with a short 
     * description of the test.
     * 
     * Each subdirectory also contains a PHP file named after the test.
     * This program makes database calls and stores the output.  This 
     * output goes into the "out" subdirectory of the test directory
     * as "out.txt".
     * 
     * When a test is created and run for the first time, the "out.txt"
     * file is copied into a "compare" subdirectory of the test.  Every
     * time the test is run, the output must match the contents of that
     * file or the test fails.
     * 
     * All tests must produce identical output for all platforms.
     * 
     * This program is hardcoded to use the database "androv2" on
     * all platforms.  Passwords and IP Numbers are in an array
     * at the top.
     * 
     * mySql trims CHAR types on returning, but other platforms do not
     * do this.  Therefore the test program must trim all values on
     * return to make sure the results are the same on all platforms.
     * 
     * @return none
     */
    function main() {
        $parm_build = true;
        if(in_array('--nobuild',$GLOBALS['argv'])) $parm_build = false;
        $parm_conntests = true;
        if(in_array('--noconntests',$GLOBALS['argv'])) $parm_conntests=false;
        
        # If tests ran ok, clear out all dd files from top
        # level before assemblying and running.  This makes
        # it easier to spot if something went wrong, because
        # these files are all ovewritten during a testing pass
        # and if something crashes they won't be there.
        $files = scandir("tests-runtime/");
        foreach($files as $file) {
            if(substr($file,-8)=='.dd.yaml') {
                unlink("tests-runtime/$file");
            }
        }
        $files = scandir("tests-runtime/debug/");
        foreach($files as $file) {
            if(substr($file,0,1)<>'.') {
                unlink("tests-runtime/debug/$file");
            }
        }
        $files = scandir("tests-runtime/out/");
        foreach($files as $file) {
            if(substr($file,0,1)<>'.') {
                unlink("tests-runtime/out/$file");
            }
        }
        
        echo "\n============================================================";
        echo "\nTriangulum Run-Time Tests";
        echo "\n============================================================";
        echo "\n";
        echo "\nThis program *assumes* that all debug tests have passed";
        echo "\nand that building the spec will not produce errors.";
        echo "\n";
        echo "\nTesting connections on each platform:";
        echo "\n";
        if(!$parm_conntests) {
            echo "Parameter --noconntests received, skipped conn tests.";
            $retval = true;
        }
        else {
            $retval = $this->mainTestsConnect();
        }
        
        
        echo "\n\n";
        # Step 1: Assemble the specs
        if($retval && $parm_build) {
            echo "\nStep 1: Assembling the specs";
            $this->mainAssemble();
        
            # Step 2: Generate a build script
            echo "\nStep 2: Generating build script";
            # This works on Linux
            `php triBuild.php --yaml=./tests-runtime/tests-runtime.dd.yaml`;
            # This is needed for windows
            $file1out = array();
            #$argv = array('','tests-runtime');
            #exec('c:\xampp\php\php-cgi.exe file1.php tests-runtime',$fileout);
            #include("triBuild.php");
            #if(count($file1out)>0) {
            #    print_r($file1out);
            #    return false;
            #}
        
            # Step 3: Drop, create, and build on all platforms
            echo "\nStep 3: Creating script on various platforms";
            $retval = $this->mainCreate();
        }
        
        # Step 4: Recurse platforms and run all tests
        if($retval) {
            echo "\nStep 4: Running actual scripts";
            foreach($this->platforms as $platform) {
                $this->mainTests($platform);
            }
        }
        
        if($retval) {
        ?>
        
TEST RESULTS
 - 'PHP' means no PHP script found
 - 'DESC' means no desc.txt script found
 - 'COMP' means no compare/out.txt file found
 - '---' means the test is ignored on this platform
 
TEST                     PG  
----------------------------------------------------------
    <?php 
        foreach($this->results as $dir=>$platforms) {
            $line =str_pad($dir,25,' ',STR_PAD_RIGHT)
                  .$platforms['postgres'];
            /*
            $line.=str_pad($platforms['db2']  ,5,' ');
            $line.=str_pad($platforms['mssql'],7,' ');
            $line.=str_pad($platforms['mysql'],15,' ');
            */
            echo "\n$line";
        }
        }
        echo "\n\n";
    }
    
    function mainTestsConnect() {
        $retval = 1;

        $dbx = 'dbname=postgres'
            .' host=localhost'
            .' user=postgres'
            .' password=postgres';
        $this->dbh = new dbh($dbx);
        echo "\n    dropping database tritests....";
        $this->dbh->query('drop database tritests');
        echo "\n    creating database tritests....";
        $this->dbh->query('create database tritests');
        $this->dbh = false;
        
      	$this->mainConnect();
   		$this->mainConnect(true);
           
        return $retval;
    }
    
    function mainConnect($second=false) {
        $dbx = 'dbname=tritests'
            .' host=localhost'
            .' user=postgres'
            .' password=postgres';
        if($second) {
            $this->dbh2 = new dbh($dbx);
        }
        else {
            $this->dbh = new dbh($dbx);
        }
    }
    
    /**
     * Assemble a grand unified test spec.
     * 
     * Copy the spec files from all tests into this directory, and 
     * generate "tests-runtime.dd.yaml" that has an "includes" directive
     * to include them all.  This grand unified spec is used to build
     * a database that has all of the tables for all tests.
     * 
     * @return null
     */
    function mainAssemble() {
        # Step 1 is to assembly a spec and generate the build script
        $includes = array();
        $dirs = scandir('./tests-runtime');
        foreach($dirs as $dir) {
        	#echo "\n Looking at directory $dir";
            if(substr($dir,0,1)=='.') continue;
            if(!is_dir('./tests-runtime/'.$dir)) continue;
            if($dir=='debug') continue;
            if($dir=='out')   continue;
            
            $file = "./tests-runtime/$dir/$dir.dd.yaml";
            if(!file_exists($file)) {
                if(file_exists("tests-runtime/$dir/test.dd.yaml")) {
                    $includes[] = $dir;
                    $fc = file_get_contents("tests-runtime/$dir/test.dd.yaml");
                    file_put_contents("tests-runtime/$dir.dd.yaml",$fc);
                }
            }
            else {
                $includes[] = $dir;
                $fc = file_get_contents($file);
                file_put_contents("tests-runtime/$dir.dd.yaml",$fc);
            }
        }
        
        $master = "includes:\n    - ".implode("\n    - ",$includes);
        file_put_contents("tests-runtime/tests-runtime.dd.yaml",$master);
    }
    
    /**
     * Creates empty databases on all platforms
     * @return unknown_type
     */
    function mainCreate() {
        $retval = true;
        foreach($this->platforms as $platform) {
            $retval = $retval && $this->mainCreateOne($platform);
        }
        return $retval;
    }
    
    function mainCreateOne($platform) {
        $retval = true;
        #$this->mainConnect($platform);
        echo "\n";
        echo "\n  Recreating empty database on $platform";
        $lines = explode(
            "-- COMMAND --"
           ,file_get_contents("tests-runtime/out/build.$platform.sql")
        );
        $ok = 0;
        $todo=0;
        foreach($lines as $idx=>$oneline) {
            if(trim($oneline)=='') continue;
            $todo++;
            #echo "\nExecuting Line $idx";
            #echo "\n";
            #print_r($oneline);
            while(substr($oneline,-1)=="\n") 
                $oneline = rtrim($oneline,"\n");
            #$oneline = str_replace("\n","\n\r",$oneline);
            $this->dbh->exec($oneline);
            $errinfo = $this->dbh->errorInfo();
            if($errinfo[0]=='00000') {
                $ok++;
            }
            else {
                $retval = false;
                echo "\n\nERROR ON COMMAND:\n";
                echo $oneline;
                echo "\n";
                print_r($this->dbh->errorInfo());
                echo "\n\n";     
                break;           
            }
        } 
        $todo = str_pad($todo,5,' ',STR_PAD_LEFT);
        $took = str_pad($ok,5,' ',STR_PAD_LEFT);
        echo "\n  Total commands to execute: ".$todo;
        echo "\n  Lines executed w/o error : ".$took."\n";
        return $retval;
    }
    
    /**
     * Recurse platforms, recurse tests, report to console.
     * 
     * The loop that goes through platforms is up in {@link main}, this
     * routine executes the tests for the given platform.
     * 
     * @return unknown_type
     */
    function mainTests($platform) {
        echo "\n\nPlatform: $platform\n";
        $this->platform = $platform;
        $this->mainConnect($platform,true);
        $this->mainConnect($platform,true,'2');
        
        # Now scan and run all tests
        $dir0 = './tests-runtime/';
        $dirs = scandir($dir0);
        
        foreach($dirs as $dir) {
            if(substr($dir,0,1)=='.') continue;
            if(!is_dir('./tests-runtime/'.$dir)) continue;
            if($dir=='debug') continue;
            if($dir=='out')   continue;

            $this->err[$platform] = array();
            
            # If there is a flag file telling us to ignore
            # this platform, then stop now
            if(file_exists($dir0.$dir."/ignore.$platform")) {
                $this->results[$dir][$platform] = '---';
                continue;
            }


            # Initalize the output for this test
            $this->out = array();

            $file = $dir0.$dir."/$dir.php";
            if(!file_exists($dir0.$dir."/$dir.php")) {
                $this->results[$dir][$platform] = 'php';
                continue;
            }

            if(!file_exists($dir0.$dir."/desc.txt")) {
                $this->results[$dir][$platform] = 'desc';
                continue;
            }

            $desc = file_get_contents($dir0.$dir."/desc.txt");
            if(substr($desc,-1)<>"\n") $desc.="\n";
            echo "$platform $desc";
            $this->results[$dir]['desc'] = $desc;
                #= file_get_contents($dir0.$dir."/desc.txt");
            
            # Initialize this global function
            include($file);
            
            if(!is_dir($dir0.$dir."/out")) {
                mkdir($dir0.$dir."/out");
            }
            
            # KFD 6/3/09 also output errors
            #            Do this before regular output because 
            #            $result is used for comparison tests
            ob_start();
            print_r($this->err[$platform]);
            $result = ob_get_clean();
            file_put_contents($dir0.$dir."/out/err.$platform.txt",$result);
            
            ob_start();
            print_r($this->out);
            $result = ob_get_clean();
            file_put_contents($dir0.$dir."/out/out.$platform.txt",$result);
            
            # Determine if the test failed, passed, or cannot be tested
            # The default result is could not be tested
            $this->results[$dir][$platform] = 'comp';
            if(file_exists($dir0.$dir."/compare/out.$platform.txt")) {
                $compare = file_get_contents(
                    $dir0.$dir."/compare/out.$platform.txt"
                );
                if($result==$compare) {
                    $this->results[$dir][$platform] = 'PASS';
                }
                else {
                    $this->results[$dir][$platform] = 'fail';
                }
            }
            else if(file_exists($dir0.$dir."/compare/out.txt")) {
                $compare = file_get_contents($dir0.$dir."/compare/out.txt");
                if($result==$compare) {
                    $this->results[$dir][$platform] = 'PASS';
                }
                else {
                    $this->results[$dir][$platform] = 'fail';
                }
            }
        }
    }
    
    function exec($string,$second = false) {
        if($second) {
            $result = $this->dbh2->exec($string);
            $err = $this->dbh2->errorInfo();
        }
        else {
            $result = $this->dbh->exec($string);
            $err = $this->dbh->errorInfo();
        }
        if($err[0]<>'00000') {
            $this->err[$this->platform][] = $err;
            if(isset($this->errmaps[$this->platform][$err[0]])) {
                $newval=$this->errmaps[$this->platform][$err[0]];
                if($newval=='CUSTOM') {
                    if($this->platform<>'mysql') {
                        $matches = array();
                        preg_match("/\{.*\}/",$err[2],$matches);
                        $newval.='  '.$matches[0];
                    }
                    else {
                        $matches = array();
                        preg_match("/PROCEDURE\s*.*\sdoes\snot\sexist/",$err[2],$matches);
                        $temp = substr($matches[0],0,strlen($matches[0])-15);
                        $temp = substr($temp,strpos($temp,"TRI_"));
                        $newval.='  {'.str_replace("__",":",$temp)."}";
                    }
                }
                $this->out[]="ERROR: ".$newval;
            }
            else {
                $this->out[]="ERROR: ".$err[0]
                    ." (NO ENTRY in \$this->errmaps[{$this->platform}]";
            }
            #$err2 = explode(';',$err[2]);
            #$err[2] = array_shift($err2);
            #$this->out[] = $err;
            #print_r($err);
        }
    }
    
    /**
     * Execute a query and return all rows.
     * 
     * @param $string
     * @return $Array an array of associative arrays.
     */
    function query($string,$second=false,$opts=array()) {
        $clips = isset($opts['clips']) ? $opts['clips'] : array();
        $retval = array();
        if($second) {
            $result = $this->dbh2->query($string,PDO::FETCH_ASSOC);
        }
        else {
            $result = $this->dbh->query($string,PDO::FETCH_ASSOC);
        }
        foreach($result as $row) {
            foreach($row as $idx=>$item) {
                # This is a hack because MySQL always trims
                # return values, even char (not just varchar),
                # so this makes all platforms come out like MySQL
                $row[$idx] = rtrim($item);
                
                # This is a hack for MS SQL, which returns
                # decimal zeros as '.00' instead of '0.00'
                if($row[$idx]==='.00') {
                     $row[$idx] = '0.00';   
                }

                if(isset($clips[$idx])) {
                    $row[$idx] = substr($item,0,$clips[$idx]);
                }
            }
            $retval[] = $row;
        }
        return $retval;
   }
}

class dbh {
    function dbh($string) {
        $this->dbx = pg_connect($string);
    }

    public function exec($sql) {
        $this->dbr = pg_query($this->dbx,$sql);
    }

    public function query($sql) {
        $dbr = pg_query($this->dbx,$sql);
        return pg_fetch_all($dbr);
        
    }
    
    public function errorinfo() {
        $err = pg_last_error($this->dbx);
        echo "\nHere is the last error\n";
        print_r($err);
        if(!$err) return array('00000'); else return array($err);
    }
}
?>
